﻿namespace PicFingerprint
{
    using System.Collections.Generic;
    using System.Drawing;
    using System.IO;
    using Util;

    class SimilarPicBuilder
    {
        private const string kOutputFileName = "data.js";

        private string rootPath;
        private string outputPath;
        private int diffThreashold;
        private List<PicInfo> pics;
        private int numProcessedImages;
        private List<PicInfoGroup> groups;

        public SimilarPicBuilder(string rp, string op, int dt)
        {
            rootPath = rp;
            outputPath = op;
            diffThreashold = dt;

            pics = new List<PicInfo>();
            numProcessedImages = 0;
            groups = new List<PicInfoGroup>();
        }

        public void Build()
        {
            DirectoryInfo dir = new DirectoryInfo(rootPath);
            FileManager.ForEachFile(dir, GetFingerPrintHandler);
            pics.Sort();
            FindSimilarPicGroups();
            ReduceGroups();
            WriteResult();
        }

        private void GetFingerPrintHandler(FileInfo f, params object[] args)
        {
            try
            {
                PicInfo info = new PicInfo(numProcessedImages, f);
                pics.Add(info);

                ++numProcessedImages;
                if ((numProcessedImages & 63) == 0)
                {
                    System.Console.WriteLine("Processed " + numProcessedImages + " images.");
                }
            }
            catch { }
        }

        private void FindSimilarPicGroups()
        {
            const int kBits = 64;
            int[] numOnesStart = new int[kBits + 1];
            for (int i = 0; i <= kBits; ++i)
            {
                numOnesStart[i] = Generic.LowerBound(pics, new PicInfo(i));
            }

            for (int i = 0; i < pics.Count; ++i)
            {
                PicInfoGroup g = new PicInfoGroup();
                g.AddPic(pics[i]);

                int lowerIdx = pics[i].NumOnesInHashCode - diffThreashold;
                if (lowerIdx < 0) lowerIdx = 0;
                int l = numOnesStart[lowerIdx];

                for (int j = l; j < i; ++j)
                {
                    int hamming = GetHammingDist(pics[i].PerceptionHashCode, pics[j].PerceptionHashCode);
                    if (hamming <= diffThreashold)
                    {
                        g.AddPic(pics[j]);
                        groups[j].AddPic(pics[i]);
                    }
                }
                groups.Add(g);
            }
        }

        private void ReduceGroups()
        {
            foreach (PicInfoGroup g in groups)
            {
                g.Sort();
            }
            groups.Sort();

            for (int i = 1; i < groups.Count; ++i)
            {
                if (groups[i].Pics.Count < 2 || groups[i].CompareTo(groups[i - 1]) == 0)
                {
                    groups[i].Duplicated = true;
                }
            }

            foreach (PicInfoGroup g in groups)
            {
                foreach (PicInfo p in g.Pics)
                {
                    p.InGroup = true;
                }
            }
        }

        private void WriteResult()
        {
            FileStream fs = new FileStream(Path.Combine(outputPath, kOutputFileName), FileMode.Create);
            StreamWriter sw = new StreamWriter(fs);
            sw.WriteLine("var data = {");

            sw.WriteLine("  image_groups: [");
            for (int i = 0; i < groups.Count; ++i)
            {
                if (!groups[i].Duplicated)
                {
                    List<PicInfo> groupPics = groups[i].Pics;
                    sw.Write("    [" + groupPics[0].Id);
                    for (int j = 1; j < groupPics.Count; ++j)
                    {
                        sw.Write(", " + groupPics[j].Id);
                    }
                    sw.WriteLine("],");
                }
            }
            sw.WriteLine("  ],");

            pics.Sort(new PicInfoCompareById());
            // sw.WriteLine("  images: {");
            sw.WriteLine("  images: [");
            for (int i = 0; i < pics.Count; ++i)
            {
                // if (pics[i].InGroup)
                // {
                //     sw.Write("    " + pics[i].Id + ": [");
                    sw.Write("    [");
                    sw.Write("'" + pics[i].Path.Replace("\\", "\\\\") + "'");
                    sw.Write(", 0");  // Whether seleted to be deleted.
                    sw.Write(", '{0,16:X16}'", pics[i].PerceptionHashCode);
                    sw.Write(", " + pics[i].Id);
                    sw.WriteLine("],");
                // }
            }
            // sw.WriteLine("  },");
            sw.WriteLine("  ],");

            sw.WriteLine("};");
            sw.Flush();
            sw.Close();
            fs.Close();
        }

        private int GetHammingDist(ulong h1, ulong h2)
        {
            int h = 0;
            ulong d = h1 ^ h2;
            while (d != 0)
            {
                ++h;
                d &= d - 1;
            }
            return h;
        }
    }
}
